Module modules.frames

File name: framse.py Author: Martin Jůda Python Version: 3.7 Description: GUI frames and their settings

Expand source code
"""
    File name: framse.py
    Author: Martin Jůda
    Python Version: 3.7
    Description: GUI frames and their settings
"""

import random
import time
import tkinter as tk
from base64 import b64decode
from datetime import datetime, timedelta
from threading import Thread
from tkinter import font as tkFont

import MFRC522.MFRC522 as MFRC522  # dependency: https://github.com/lthiery/SPI-Py
import RPi.GPIO as GPIO
from PIL import ImageTk, Image

from modules.config import (
    NUMBER_OF_QUESTIONS,
    DEVICE_ID,
    USER_INACTIVITY_SECONDS,
    ERROR_TEXT,
    QUESTION_SWITCH_SECONDS,
    DOOR_OPEN_SECONDS,
    RELAY_OUTPUT_PIN,
)
from modules.constants import QUESTIONS_URL, Errors, PICTURE_URL
from modules.shared import (
    AsyncBackendCommunicator,
    ActionInterface,
    ReadCardError,
)


def read_card_id():
    """Read Card ID from sensor

    Returns:
        str: Card number

    Raises:
        ReadCardError: when card is not presented

    """

    card_reader = MFRC522.MFRC522()
    (status, TagType) = card_reader.MFRC522_Request(card_reader.PICC_REQIDL)
    if status != card_reader.MI_OK:
        card_reader.close_spi()
        raise ReadCardError("Error when reading card.")
    (status, uid) = card_reader.MFRC522_Anticoll()
    card_reader.close_spi()
    if status == card_reader.MI_OK:
        return f"{uid[0]:02x}{uid[1]:02x}{uid[2]:02x}{uid[3]:02x}".upper()
    raise ReadCardError("Error when reading card.")


def read_card(read_card_label, controller):
    """Read card periodic controller, read card and if is not presented plan another
    read. If card number is red then frame is switched.

    Args:
        read_card_label (tk.Label): Label for which is planned after action
        controller (GUI): class for switch to another frame

    """
    try:
        card_number = read_card_id()
    except ReadCardError:
        read_card_label.after(200, read_card, read_card_label, controller)
    else:
        controller.switch_frame_by_class(VerifyCardPage, card_number=card_number)
        return


class ReadCardPage(tk.Frame, ActionInterface):
    """Read card page"""

    def __init__(self, parent, *args, **kwargs):
        super(ReadCardPage, self).__init__(parent)
        self.parent = parent
        self.read_card_label = tk.Label(  # Read label
            self,
            text="Přiložte vstupní kartu",
            fg="black",
            font=tkFont.Font(family="Helvetica", size=45, weight="bold"),
        )
        # Center label position and grid settings
        self.read_card_label.grid(column=1, row=1, sticky="nsew")
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(1, weight=1)

    def post_init_actions(self):
        """Plan periodic card read"""
        read_card(self.read_card_label, self.parent)


class VerifyCardPage(tk.Frame, ActionInterface):
    """Verify card number page"""

    def __init__(self, parent, *args, **kwargs):
        super(VerifyCardPage, self).__init__(parent)
        self.parent = parent
        self.card_number = kwargs.get("card_number")
        # Info label
        self.verify_card_label = tk.Label(
            self,
            text="Karta načtena. Probíhá ověřování její platnosti.",
            fg="black",
            font=tkFont.Font(family="Helvetica", size=45, weight="bold"),
            wraplength=1000,
            justify=tk.CENTER,
        )
        self.verify_card_label.grid(column=1, row=1)
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(1, weight=1)

    def post_init_actions(self):
        """Start async backend call and start monitoring if this operation finish"""
        backend_thread = AsyncBackendCommunicator(
            url=QUESTIONS_URL,
            daemon=True,
            params=dict(
                card_number=self.card_number,
                question_count=NUMBER_OF_QUESTIONS,
                device_id=DEVICE_ID,
            ),
        )
        backend_thread.start()
        self.monitor_backend_thread(backend_thread)

    def monitor_backend_thread(self, thread):
        """Check if async thread is finished. If it is then handle result and switch
        frame if it is not finished then plan periodic result check.

        Args:
            thread (AsyncBackendCommunicator): Started backend call thread

        """
        if thread.is_alive():
            # thread still working, plan periodic check after 100ms
            self.after(100, lambda: self.monitor_backend_thread(thread))
        else:
            if thread.is_result_ok:
                # thread finished and questions were downloaded
                if not thread.result.get("questions"):
                    self.parent.switch_frame_by_class(
                        ErrorPage,
                        error_type=Errors.NO_QUESTIONS_ERROR,
                    )
                    return
                # init questions container and show first question
                questions_container = QuestionsContainer(
                    controller=self.parent,
                    student_name=thread.result["student_name"],
                    questions=thread.result["questions"],
                )
                questions_container.show_next_question()
                return
            else:
                # thread finished with error
                if thread.error_code == 404:
                    error_type = Errors.NO_ACTIVE_STUDY_ERROR
                elif thread.error_code == 403:
                    error_type = Errors.NO_ACCESS_ERROR
                else:
                    error_type = thread.error_type
                # show error page
                self.parent.switch_frame_by_class(
                    ErrorPage,
                    error_type=error_type,
                    error_code=thread.error_code,
                )
                return


def check_user_activity(active_page, ui_element=None, seconds=None):
    """Test if frame was changed by user and if timeout expired

    Args:
        active_page: frame page instance
        ui_element: optional ui element for text change
        seconds: inactivity timeout

    """
    if active_page.user_did_action:
        # user did action then finish periodic check
        return
    else:
        # test if timeout expired
        if datetime.now() > active_page.init_time + timedelta(
            seconds=USER_INACTIVITY_SECONDS
        ):
            # switch to start page
            active_page.parent.switch_frame_by_class(ReadCardPage)
        else:
            # change text and plan next activity test
            if ui_element and seconds:
                seconds -= 1
                ui_element.config(text=str(seconds))
            active_page.after(
                1000, check_user_activity, active_page, ui_element, seconds
            )


class ErrorPage(tk.Frame, ActionInterface):
    """Error message frame"""

    def __init__(self, parent, *args, **kwargs):
        super(ErrorPage, self).__init__(parent)
        self.parent = parent
        self.user_did_action = False
        self.init_time = None

        # configure grid layout
        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)
        for i in range(5):
            self.grid_rowconfigure(i, weight=1, uniform="r")
        # set error type and labels positions
        error_texts = ERROR_TEXT[kwargs["error_type"].value]
        error_type_value_label = self.create_label(
            f"{error_texts['error_name'].format(error_code=kwargs.get('error_code'))}"
        )
        error_type_key_label = self.create_label("Typ chyby:")
        error_type_key_label.grid(column=0, row=0, stick="e", ipadx=50, ipady=50)
        error_type_value_label.grid(column=1, row=0, stick="w", ipadx=50, ipady=50)
        next_step_value_label = self.create_label(f"{error_texts['hint']}")
        next_step_value_label.grid(column=1, row=1, stick="w", ipadx=50, ipady=50)
        nex_step_key_label = self.create_label("Postup:")
        nex_step_key_label.grid(column=0, row=1, stick="e", ipadx=50, ipady=50)
        contact_value_label = self.create_label(f"{error_texts['contact']}")
        contact_value_label.grid(column=1, row=2, stick="w", ipadx=50, ipady=50)
        contact_key_label = self.create_label("Kontakt:")
        contact_key_label.grid(column=0, row=2, stick="e", ipadx=50, ipady=50)
        device_id_value_label = self.create_label(f"{DEVICE_ID}")
        device_id_value_label.grid(column=1, row=3, stick="w", ipadx=50, ipady=50)
        device_id_key_label = self.create_label("ID zařízení:")
        device_id_key_label.grid(column=0, row=3, stick="e", ipadx=50, ipady=50)
        exit_button = tk.Button(
            self,
            text="Zavřít",
            command=lambda: self.close_window(),
            padx=15,
            pady=10,
            font=tkFont.Font(family="Helvetica", size=22),
        )
        exit_button.grid(column=0, row=4, ipadx=30, ipady=30, columnspan=2, stick="n")

    def create_label(self, text):
        """Create label with given text

        Args:
            text(str): Text in label

        Returns (tk.Label): configured label

        """
        return tk.Label(
            self,
            text=text,
            fg="black",
            wraplength=870,
            justify=tk.CENTER,
            font=tkFont.Font(family="Helvetica", size=22),
        )

    def close_window(self):
        """Exit button action, set user activity and switch frame"""
        self.user_did_action = True
        self.parent.switch_frame_by_class(ReadCardPage)

    def post_init_actions(self):
        """Set init time for user inactivity and start monitoring of this activity"""
        self.init_time = datetime.now()
        self.after(1000, check_user_activity, self)


class QuestionPage(tk.Frame, ActionInterface):
    """Question page"""

    def __init__(
        self,
        parent,
        questions_container,
        question_text,
        subject_name,
        picture_id,
        answers,
        is_last_question,
        *args,
        **kwargs,
    ):
        super(QuestionPage, self).__init__(parent)
        self.parent = parent
        self.picture_id = picture_id
        self.is_last_question = is_last_question
        self.questions_container = questions_container
        self.user_did_action = False
        self.init_time = None
        self.picture = None
        self.picture_label = None
        self.picture_thread = None
        # set two column
        self.grid_columnconfigure(0, weight=1, uniform="a")
        self.grid_columnconfigure(1, weight=1, uniform="a")

        if picture_id:
            # start picture download
            self.picture_thread = AsyncBackendCommunicator(
                url=PICTURE_URL.format(picture_id=picture_id), daemon=True
            )
            self.picture_thread.start()

            # set row layout for question with picture
            self.grid_rowconfigure(0, weight=1, uniform="b")
            self.grid_rowconfigure(1, weight=4, uniform="b")
            self.grid_rowconfigure(2, weight=10, uniform="b")
            self.grid_rowconfigure(3, weight=1, uniform="b")
            self.grid_rowconfigure(4, weight=4, uniform="b")
            self.grid_rowconfigure(5, weight=4, uniform="b")
        else:
            # set row layout for question without picture
            self.grid_rowconfigure(0, weight=1, uniform="b")
            self.grid_rowconfigure(1, weight=8, uniform="b")
            self.grid_rowconfigure(2, weight=1, uniform="b")
            self.grid_rowconfigure(3, weight=4, uniform="b")
            self.grid_rowconfigure(4, weight=4, uniform="b")

        self.questions_mapping = {}
        for i in range(4):
            # set answers by button numbers
            self.questions_mapping[i] = answers[i]

        self.student_subject_label = tk.Label(
            self,
            text=f"{self.questions_container.student_name} - {subject_name}",
            fg="black",
            font=tkFont.Font(family="Helvetica", size=18, weight="bold"),
            padx=20,
            wraplength=1100,
            justify=tk.CENTER,
        )
        self.student_subject_label.grid(column=0, row=0, columnspan=2, sticky="nsew")

        self.question_text_label = tk.Label(
            self,
            text=question_text,
            fg="black",
            font=tkFont.Font(family="Helvetica", size=22, weight="bold"),
            wraplength=1100,
            justify=tk.CENTER,
            bg="white",
        )
        self.question_text_label.grid(column=0, row=1, columnspan=2, sticky="nsew")
        self.result_text = tk.Label(
            self,
            text=USER_INACTIVITY_SECONDS,
            fg="black",
            font=tkFont.Font(family="Helvetica", size=22, weight="bold"),
            wraplength=1000,
            justify=tk.CENTER,
            bg="white",
        )
        self.result_text.grid(
            column=0, row=3 if self.picture_id else 2, columnspan=2, sticky="nsew"
        )
        if self.picture_id:
            self.picture_label = tk.Label(self)
            self.picture_label.grid(column=0, row=2, columnspan=2, sticky="nsew")

        # set buttons with their texts and actions
        self.button_0 = self.create_button(0, self.questions_mapping[0]["answer"])
        self.questions_mapping[0]["button"] = self.button_0
        self.button_0.grid(column=0, row=4 if self.picture_id else 3, sticky="nsew")

        self.button_1 = self.create_button(1, self.questions_mapping[1]["answer"])
        self.questions_mapping[1]["button"] = self.button_1
        self.button_1.grid(column=0, row=5 if self.picture_id else 4, sticky="nsew")

        self.button_2 = self.create_button(2, self.questions_mapping[2]["answer"])
        self.questions_mapping[2]["button"] = self.button_2
        self.button_2.grid(column=1, row=4 if self.picture_id else 3, sticky="nsew")

        self.button_3 = self.create_button(3, self.questions_mapping[3]["answer"])
        self.button_3.grid(column=1, row=5 if self.picture_id else 4, sticky="nsew")
        self.questions_mapping[3]["button"] = self.button_3

    def create_button(self, button_id, text):
        """Create configured button

        Args:
            button_id (int): button id
            text (str): button text

        Returns:
            tk.Button: Button with text and set click action

        """
        return tk.Button(
            self,
            text=text,
            command=lambda: self.handle_button_click(button_id),
            wraplength=500,
            font=tkFont.Font(family="Helvetica", size=16),
            justify=tk.CENTER,
            highlightbackground="white",
            padx=5,
            pady=5,
        )

    def handle_button_click(self, button_number):
        """Handle button click, disable all buttons and set colors

        Args:
            button_number (int): id of pressed button

        """
        self.user_did_action = True  # set flag for user action
        if self.questions_mapping[button_number]["correct"] is True:
            # correct answer pressed, open door
            self.disable_buttons()
            self.open_door(text="Správná odpověď, dveře otevřeny.")
        else:
            # set red color for wrong answer
            (self.questions_mapping[button_number]["button"]).config(
                bg="red", highlightbackground="red"
            )
            self.disable_buttons()
            if self.is_last_question:
                # last question, open door
                self.open_door(text="Chybná odpověď, zlepšete se. Dveře otevřeny.")
            else:
                # show next question
                self.render_result_label(text="Chybná odpověď, následuje další otázka.")
                self.after(
                    (QUESTION_SWITCH_SECONDS * 1000),
                    lambda: self.questions_container.show_next_question(),
                )

    def disable_buttons(self):
        """Disable click for all buttons and set green color for correct answer"""
        for i, _ in enumerate(self.questions_mapping):
            (self.questions_mapping[i]["button"]).config(
                state="disabled", disabledforeground="black"
            )
            if self.questions_mapping[i]["correct"] is True:
                (self.questions_mapping[i]["button"]).config(
                    bg="green", highlightbackground="green"
                )

    def open_door(self, text):
        """Open door and set result text and plan frame switch"""
        self.render_result_label(text=text)
        self.after(
            (QUESTION_SWITCH_SECONDS * 1000),
            lambda: self.parent.switch_frame_by_class(ReadCardPage),
        )
        OpenDoor().start()

    def render_result_label(self, text):
        """Set text for result label

        Args:
            text (str): text in label

        """
        self.result_text.config(text=text)

    def post_init_actions(self):
        """Render picture if question contain it and set init time and start user
        inactivity timeout"""
        if self.picture_id:
            if self.picture_thread.result is None:
                # question download failed
                if self.is_last_question:
                    # last question so show error
                    self.parent.switch_frame_by_class(
                        ErrorPage,
                        error_type=Errors.IMAGE_LOAD_ERROR,
                    )
                else:
                    # skip this question due to image download error
                    self.questions_container.show_next_question()
                return
            # load picture from backend response
            picture_bytes = b64decode((self.picture_thread.result["image"]).encode())
            picture = ImageTk.BytesIO(picture_bytes)
            pil_image = Image.open(picture)
            max_height = 320
            if pil_image.size[1] > max_height:
                # picture size is greater then max height, then resize it
                percent = max_height / float(pil_image.size[1])
                width_size = int((float(pil_image.size[0]) * float(percent)))
                pil_image = pil_image.resize((width_size, max_height), Image.ANTIALIAS)
            self.picture = ImageTk.PhotoImage(pil_image)
            self.picture_label.config(image=self.picture)
        self.init_time = datetime.now()
        self.after(
            1000, check_user_activity, self, self.result_text, USER_INACTIVITY_SECONDS
        )


class OpenDoor(Thread):
    """Async door open - relay switcher"""

    def __init__(self, **kwargs):
        super(OpenDoor, self).__init__(**kwargs)

    def run(self):
        """Open door, wait for timeout and then close door"""
        print("start")
        GPIO.output(RELAY_OUTPUT_PIN, GPIO.LOW)
        time.sleep(DOOR_OPEN_SECONDS)
        GPIO.output(RELAY_OUTPUT_PIN, GPIO.HIGH)
        print("END")


class QuestionsContainer:
    """Questions controller, contains all question frames and can switch between them"""

    def __init__(self, controller, student_name, questions):
        self.controller = controller
        self.student_name = student_name
        self.questions = []
        self.current_question = -1

        for i, question in enumerate(questions):
            answers = [
                {"answer": question["wrong_answer_1"], "correct": False},
                {"answer": question["wrong_answer_2"], "correct": False},
                {"answer": question["wrong_answer_3"], "correct": False},
                {"answer": question["right_answer"], "correct": True},
            ]
            # show answers in random order
            random.shuffle(answers)
            # create frame for ech question
            self.questions.append(
                QuestionPage(
                    parent=controller,
                    questions_container=self,
                    question_text=question["question"],
                    subject_name=question["subject_name"],
                    picture_id=question["picture_id"],
                    answers=answers,
                    is_last_question=True if i == len(questions) - 1 else False,
                )
            )

    def show_next_question(self):
        """Switch to another question frame"""
        self.current_question += 1
        if self.questions[self.current_question].picture_id:
            # wait for picture question download if picture is presented
            self.questions[self.current_question].picture_thread.join()
        self.controller.switch_frame_by_instance(self.questions[self.current_question])
        return

Functions

def check_user_activity(active_page, ui_element=None, seconds=None)

Test if frame was changed by user and if timeout expired

Args

active_page
frame page instance
ui_element
optional ui element for text change
seconds
inactivity timeout
Expand source code
def check_user_activity(active_page, ui_element=None, seconds=None):
    """Test if frame was changed by user and if timeout expired

    Args:
        active_page: frame page instance
        ui_element: optional ui element for text change
        seconds: inactivity timeout

    """
    if active_page.user_did_action:
        # user did action then finish periodic check
        return
    else:
        # test if timeout expired
        if datetime.now() > active_page.init_time + timedelta(
            seconds=USER_INACTIVITY_SECONDS
        ):
            # switch to start page
            active_page.parent.switch_frame_by_class(ReadCardPage)
        else:
            # change text and plan next activity test
            if ui_element and seconds:
                seconds -= 1
                ui_element.config(text=str(seconds))
            active_page.after(
                1000, check_user_activity, active_page, ui_element, seconds
            )
def read_card(read_card_label, controller)

Read card periodic controller, read card and if is not presented plan another read. If card number is red then frame is switched.

Args

read_card_label : tk.Label
Label for which is planned after action
controller : GUI
class for switch to another frame
Expand source code
def read_card(read_card_label, controller):
    """Read card periodic controller, read card and if is not presented plan another
    read. If card number is red then frame is switched.

    Args:
        read_card_label (tk.Label): Label for which is planned after action
        controller (GUI): class for switch to another frame

    """
    try:
        card_number = read_card_id()
    except ReadCardError:
        read_card_label.after(200, read_card, read_card_label, controller)
    else:
        controller.switch_frame_by_class(VerifyCardPage, card_number=card_number)
        return
def read_card_id()

Read Card ID from sensor

Returns

str
Card number

Raises

ReadCardError
when card is not presented
Expand source code
def read_card_id():
    """Read Card ID from sensor

    Returns:
        str: Card number

    Raises:
        ReadCardError: when card is not presented

    """

    card_reader = MFRC522.MFRC522()
    (status, TagType) = card_reader.MFRC522_Request(card_reader.PICC_REQIDL)
    if status != card_reader.MI_OK:
        card_reader.close_spi()
        raise ReadCardError("Error when reading card.")
    (status, uid) = card_reader.MFRC522_Anticoll()
    card_reader.close_spi()
    if status == card_reader.MI_OK:
        return f"{uid[0]:02x}{uid[1]:02x}{uid[2]:02x}{uid[3]:02x}".upper()
    raise ReadCardError("Error when reading card.")

Classes

class ErrorPage (parent, *args, **kwargs)

Error message frame

Construct a frame widget with the parent MASTER.

Valid resource names: background, bd, bg, borderwidth, class, colormap, container, cursor, height, highlightbackground, highlightcolor, highlightthickness, relief, takefocus, visual, width.

Expand source code
class ErrorPage(tk.Frame, ActionInterface):
    """Error message frame"""

    def __init__(self, parent, *args, **kwargs):
        super(ErrorPage, self).__init__(parent)
        self.parent = parent
        self.user_did_action = False
        self.init_time = None

        # configure grid layout
        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)
        for i in range(5):
            self.grid_rowconfigure(i, weight=1, uniform="r")
        # set error type and labels positions
        error_texts = ERROR_TEXT[kwargs["error_type"].value]
        error_type_value_label = self.create_label(
            f"{error_texts['error_name'].format(error_code=kwargs.get('error_code'))}"
        )
        error_type_key_label = self.create_label("Typ chyby:")
        error_type_key_label.grid(column=0, row=0, stick="e", ipadx=50, ipady=50)
        error_type_value_label.grid(column=1, row=0, stick="w", ipadx=50, ipady=50)
        next_step_value_label = self.create_label(f"{error_texts['hint']}")
        next_step_value_label.grid(column=1, row=1, stick="w", ipadx=50, ipady=50)
        nex_step_key_label = self.create_label("Postup:")
        nex_step_key_label.grid(column=0, row=1, stick="e", ipadx=50, ipady=50)
        contact_value_label = self.create_label(f"{error_texts['contact']}")
        contact_value_label.grid(column=1, row=2, stick="w", ipadx=50, ipady=50)
        contact_key_label = self.create_label("Kontakt:")
        contact_key_label.grid(column=0, row=2, stick="e", ipadx=50, ipady=50)
        device_id_value_label = self.create_label(f"{DEVICE_ID}")
        device_id_value_label.grid(column=1, row=3, stick="w", ipadx=50, ipady=50)
        device_id_key_label = self.create_label("ID zařízení:")
        device_id_key_label.grid(column=0, row=3, stick="e", ipadx=50, ipady=50)
        exit_button = tk.Button(
            self,
            text="Zavřít",
            command=lambda: self.close_window(),
            padx=15,
            pady=10,
            font=tkFont.Font(family="Helvetica", size=22),
        )
        exit_button.grid(column=0, row=4, ipadx=30, ipady=30, columnspan=2, stick="n")

    def create_label(self, text):
        """Create label with given text

        Args:
            text(str): Text in label

        Returns (tk.Label): configured label

        """
        return tk.Label(
            self,
            text=text,
            fg="black",
            wraplength=870,
            justify=tk.CENTER,
            font=tkFont.Font(family="Helvetica", size=22),
        )

    def close_window(self):
        """Exit button action, set user activity and switch frame"""
        self.user_did_action = True
        self.parent.switch_frame_by_class(ReadCardPage)

    def post_init_actions(self):
        """Set init time for user inactivity and start monitoring of this activity"""
        self.init_time = datetime.now()
        self.after(1000, check_user_activity, self)

Ancestors

  • tkinter.Frame
  • tkinter.Widget
  • tkinter.BaseWidget
  • tkinter.Misc
  • tkinter.Pack
  • tkinter.Place
  • tkinter.Grid
  • ActionInterface
  • abc.ABC

Methods

def close_window(self)

Exit button action, set user activity and switch frame

Expand source code
def close_window(self):
    """Exit button action, set user activity and switch frame"""
    self.user_did_action = True
    self.parent.switch_frame_by_class(ReadCardPage)
def create_label(self, text)

Create label with given text

Args

text(str): Text in label Returns (tk.Label): configured label

Expand source code
def create_label(self, text):
    """Create label with given text

    Args:
        text(str): Text in label

    Returns (tk.Label): configured label

    """
    return tk.Label(
        self,
        text=text,
        fg="black",
        wraplength=870,
        justify=tk.CENTER,
        font=tkFont.Font(family="Helvetica", size=22),
    )
def post_init_actions(self)

Set init time for user inactivity and start monitoring of this activity

Expand source code
def post_init_actions(self):
    """Set init time for user inactivity and start monitoring of this activity"""
    self.init_time = datetime.now()
    self.after(1000, check_user_activity, self)
class OpenDoor (**kwargs)

Async door open - relay switcher

This constructor should always be called with keyword arguments. Arguments are:

group should be None; reserved for future extension when a ThreadGroup class is implemented.

target is the callable object to be invoked by the run() method. Defaults to None, meaning nothing is called.

name is the thread name. By default, a unique name is constructed of the form "Thread-N" where N is a small decimal number.

args is the argument tuple for the target invocation. Defaults to ().

kwargs is a dictionary of keyword arguments for the target invocation. Defaults to {}.

If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.init()) before doing anything else to the thread.

Expand source code
class OpenDoor(Thread):
    """Async door open - relay switcher"""

    def __init__(self, **kwargs):
        super(OpenDoor, self).__init__(**kwargs)

    def run(self):
        """Open door, wait for timeout and then close door"""
        print("start")
        GPIO.output(RELAY_OUTPUT_PIN, GPIO.LOW)
        time.sleep(DOOR_OPEN_SECONDS)
        GPIO.output(RELAY_OUTPUT_PIN, GPIO.HIGH)
        print("END")

Ancestors

  • threading.Thread

Methods

def run(self)

Open door, wait for timeout and then close door

Expand source code
def run(self):
    """Open door, wait for timeout and then close door"""
    print("start")
    GPIO.output(RELAY_OUTPUT_PIN, GPIO.LOW)
    time.sleep(DOOR_OPEN_SECONDS)
    GPIO.output(RELAY_OUTPUT_PIN, GPIO.HIGH)
    print("END")
class QuestionPage (parent, questions_container, question_text, subject_name, picture_id, answers, is_last_question, *args, **kwargs)

Question page

Construct a frame widget with the parent MASTER.

Valid resource names: background, bd, bg, borderwidth, class, colormap, container, cursor, height, highlightbackground, highlightcolor, highlightthickness, relief, takefocus, visual, width.

Expand source code
class QuestionPage(tk.Frame, ActionInterface):
    """Question page"""

    def __init__(
        self,
        parent,
        questions_container,
        question_text,
        subject_name,
        picture_id,
        answers,
        is_last_question,
        *args,
        **kwargs,
    ):
        super(QuestionPage, self).__init__(parent)
        self.parent = parent
        self.picture_id = picture_id
        self.is_last_question = is_last_question
        self.questions_container = questions_container
        self.user_did_action = False
        self.init_time = None
        self.picture = None
        self.picture_label = None
        self.picture_thread = None
        # set two column
        self.grid_columnconfigure(0, weight=1, uniform="a")
        self.grid_columnconfigure(1, weight=1, uniform="a")

        if picture_id:
            # start picture download
            self.picture_thread = AsyncBackendCommunicator(
                url=PICTURE_URL.format(picture_id=picture_id), daemon=True
            )
            self.picture_thread.start()

            # set row layout for question with picture
            self.grid_rowconfigure(0, weight=1, uniform="b")
            self.grid_rowconfigure(1, weight=4, uniform="b")
            self.grid_rowconfigure(2, weight=10, uniform="b")
            self.grid_rowconfigure(3, weight=1, uniform="b")
            self.grid_rowconfigure(4, weight=4, uniform="b")
            self.grid_rowconfigure(5, weight=4, uniform="b")
        else:
            # set row layout for question without picture
            self.grid_rowconfigure(0, weight=1, uniform="b")
            self.grid_rowconfigure(1, weight=8, uniform="b")
            self.grid_rowconfigure(2, weight=1, uniform="b")
            self.grid_rowconfigure(3, weight=4, uniform="b")
            self.grid_rowconfigure(4, weight=4, uniform="b")

        self.questions_mapping = {}
        for i in range(4):
            # set answers by button numbers
            self.questions_mapping[i] = answers[i]

        self.student_subject_label = tk.Label(
            self,
            text=f"{self.questions_container.student_name} - {subject_name}",
            fg="black",
            font=tkFont.Font(family="Helvetica", size=18, weight="bold"),
            padx=20,
            wraplength=1100,
            justify=tk.CENTER,
        )
        self.student_subject_label.grid(column=0, row=0, columnspan=2, sticky="nsew")

        self.question_text_label = tk.Label(
            self,
            text=question_text,
            fg="black",
            font=tkFont.Font(family="Helvetica", size=22, weight="bold"),
            wraplength=1100,
            justify=tk.CENTER,
            bg="white",
        )
        self.question_text_label.grid(column=0, row=1, columnspan=2, sticky="nsew")
        self.result_text = tk.Label(
            self,
            text=USER_INACTIVITY_SECONDS,
            fg="black",
            font=tkFont.Font(family="Helvetica", size=22, weight="bold"),
            wraplength=1000,
            justify=tk.CENTER,
            bg="white",
        )
        self.result_text.grid(
            column=0, row=3 if self.picture_id else 2, columnspan=2, sticky="nsew"
        )
        if self.picture_id:
            self.picture_label = tk.Label(self)
            self.picture_label.grid(column=0, row=2, columnspan=2, sticky="nsew")

        # set buttons with their texts and actions
        self.button_0 = self.create_button(0, self.questions_mapping[0]["answer"])
        self.questions_mapping[0]["button"] = self.button_0
        self.button_0.grid(column=0, row=4 if self.picture_id else 3, sticky="nsew")

        self.button_1 = self.create_button(1, self.questions_mapping[1]["answer"])
        self.questions_mapping[1]["button"] = self.button_1
        self.button_1.grid(column=0, row=5 if self.picture_id else 4, sticky="nsew")

        self.button_2 = self.create_button(2, self.questions_mapping[2]["answer"])
        self.questions_mapping[2]["button"] = self.button_2
        self.button_2.grid(column=1, row=4 if self.picture_id else 3, sticky="nsew")

        self.button_3 = self.create_button(3, self.questions_mapping[3]["answer"])
        self.button_3.grid(column=1, row=5 if self.picture_id else 4, sticky="nsew")
        self.questions_mapping[3]["button"] = self.button_3

    def create_button(self, button_id, text):
        """Create configured button

        Args:
            button_id (int): button id
            text (str): button text

        Returns:
            tk.Button: Button with text and set click action

        """
        return tk.Button(
            self,
            text=text,
            command=lambda: self.handle_button_click(button_id),
            wraplength=500,
            font=tkFont.Font(family="Helvetica", size=16),
            justify=tk.CENTER,
            highlightbackground="white",
            padx=5,
            pady=5,
        )

    def handle_button_click(self, button_number):
        """Handle button click, disable all buttons and set colors

        Args:
            button_number (int): id of pressed button

        """
        self.user_did_action = True  # set flag for user action
        if self.questions_mapping[button_number]["correct"] is True:
            # correct answer pressed, open door
            self.disable_buttons()
            self.open_door(text="Správná odpověď, dveře otevřeny.")
        else:
            # set red color for wrong answer
            (self.questions_mapping[button_number]["button"]).config(
                bg="red", highlightbackground="red"
            )
            self.disable_buttons()
            if self.is_last_question:
                # last question, open door
                self.open_door(text="Chybná odpověď, zlepšete se. Dveře otevřeny.")
            else:
                # show next question
                self.render_result_label(text="Chybná odpověď, následuje další otázka.")
                self.after(
                    (QUESTION_SWITCH_SECONDS * 1000),
                    lambda: self.questions_container.show_next_question(),
                )

    def disable_buttons(self):
        """Disable click for all buttons and set green color for correct answer"""
        for i, _ in enumerate(self.questions_mapping):
            (self.questions_mapping[i]["button"]).config(
                state="disabled", disabledforeground="black"
            )
            if self.questions_mapping[i]["correct"] is True:
                (self.questions_mapping[i]["button"]).config(
                    bg="green", highlightbackground="green"
                )

    def open_door(self, text):
        """Open door and set result text and plan frame switch"""
        self.render_result_label(text=text)
        self.after(
            (QUESTION_SWITCH_SECONDS * 1000),
            lambda: self.parent.switch_frame_by_class(ReadCardPage),
        )
        OpenDoor().start()

    def render_result_label(self, text):
        """Set text for result label

        Args:
            text (str): text in label

        """
        self.result_text.config(text=text)

    def post_init_actions(self):
        """Render picture if question contain it and set init time and start user
        inactivity timeout"""
        if self.picture_id:
            if self.picture_thread.result is None:
                # question download failed
                if self.is_last_question:
                    # last question so show error
                    self.parent.switch_frame_by_class(
                        ErrorPage,
                        error_type=Errors.IMAGE_LOAD_ERROR,
                    )
                else:
                    # skip this question due to image download error
                    self.questions_container.show_next_question()
                return
            # load picture from backend response
            picture_bytes = b64decode((self.picture_thread.result["image"]).encode())
            picture = ImageTk.BytesIO(picture_bytes)
            pil_image = Image.open(picture)
            max_height = 320
            if pil_image.size[1] > max_height:
                # picture size is greater then max height, then resize it
                percent = max_height / float(pil_image.size[1])
                width_size = int((float(pil_image.size[0]) * float(percent)))
                pil_image = pil_image.resize((width_size, max_height), Image.ANTIALIAS)
            self.picture = ImageTk.PhotoImage(pil_image)
            self.picture_label.config(image=self.picture)
        self.init_time = datetime.now()
        self.after(
            1000, check_user_activity, self, self.result_text, USER_INACTIVITY_SECONDS
        )

Ancestors

  • tkinter.Frame
  • tkinter.Widget
  • tkinter.BaseWidget
  • tkinter.Misc
  • tkinter.Pack
  • tkinter.Place
  • tkinter.Grid
  • ActionInterface
  • abc.ABC

Methods

def create_button(self, button_id, text)

Create configured button

Args

button_id : int
button id
text : str
button text

Returns

tk.Button
Button with text and set click action
Expand source code
def create_button(self, button_id, text):
    """Create configured button

    Args:
        button_id (int): button id
        text (str): button text

    Returns:
        tk.Button: Button with text and set click action

    """
    return tk.Button(
        self,
        text=text,
        command=lambda: self.handle_button_click(button_id),
        wraplength=500,
        font=tkFont.Font(family="Helvetica", size=16),
        justify=tk.CENTER,
        highlightbackground="white",
        padx=5,
        pady=5,
    )
def disable_buttons(self)

Disable click for all buttons and set green color for correct answer

Expand source code
def disable_buttons(self):
    """Disable click for all buttons and set green color for correct answer"""
    for i, _ in enumerate(self.questions_mapping):
        (self.questions_mapping[i]["button"]).config(
            state="disabled", disabledforeground="black"
        )
        if self.questions_mapping[i]["correct"] is True:
            (self.questions_mapping[i]["button"]).config(
                bg="green", highlightbackground="green"
            )
def handle_button_click(self, button_number)

Handle button click, disable all buttons and set colors

Args

button_number : int
id of pressed button
Expand source code
def handle_button_click(self, button_number):
    """Handle button click, disable all buttons and set colors

    Args:
        button_number (int): id of pressed button

    """
    self.user_did_action = True  # set flag for user action
    if self.questions_mapping[button_number]["correct"] is True:
        # correct answer pressed, open door
        self.disable_buttons()
        self.open_door(text="Správná odpověď, dveře otevřeny.")
    else:
        # set red color for wrong answer
        (self.questions_mapping[button_number]["button"]).config(
            bg="red", highlightbackground="red"
        )
        self.disable_buttons()
        if self.is_last_question:
            # last question, open door
            self.open_door(text="Chybná odpověď, zlepšete se. Dveře otevřeny.")
        else:
            # show next question
            self.render_result_label(text="Chybná odpověď, následuje další otázka.")
            self.after(
                (QUESTION_SWITCH_SECONDS * 1000),
                lambda: self.questions_container.show_next_question(),
            )
def open_door(self, text)

Open door and set result text and plan frame switch

Expand source code
def open_door(self, text):
    """Open door and set result text and plan frame switch"""
    self.render_result_label(text=text)
    self.after(
        (QUESTION_SWITCH_SECONDS * 1000),
        lambda: self.parent.switch_frame_by_class(ReadCardPage),
    )
    OpenDoor().start()
def post_init_actions(self)

Render picture if question contain it and set init time and start user inactivity timeout

Expand source code
def post_init_actions(self):
    """Render picture if question contain it and set init time and start user
    inactivity timeout"""
    if self.picture_id:
        if self.picture_thread.result is None:
            # question download failed
            if self.is_last_question:
                # last question so show error
                self.parent.switch_frame_by_class(
                    ErrorPage,
                    error_type=Errors.IMAGE_LOAD_ERROR,
                )
            else:
                # skip this question due to image download error
                self.questions_container.show_next_question()
            return
        # load picture from backend response
        picture_bytes = b64decode((self.picture_thread.result["image"]).encode())
        picture = ImageTk.BytesIO(picture_bytes)
        pil_image = Image.open(picture)
        max_height = 320
        if pil_image.size[1] > max_height:
            # picture size is greater then max height, then resize it
            percent = max_height / float(pil_image.size[1])
            width_size = int((float(pil_image.size[0]) * float(percent)))
            pil_image = pil_image.resize((width_size, max_height), Image.ANTIALIAS)
        self.picture = ImageTk.PhotoImage(pil_image)
        self.picture_label.config(image=self.picture)
    self.init_time = datetime.now()
    self.after(
        1000, check_user_activity, self, self.result_text, USER_INACTIVITY_SECONDS
    )
def render_result_label(self, text)

Set text for result label

Args

text : str
text in label
Expand source code
def render_result_label(self, text):
    """Set text for result label

    Args:
        text (str): text in label

    """
    self.result_text.config(text=text)
class QuestionsContainer (controller, student_name, questions)

Questions controller, contains all question frames and can switch between them

Expand source code
class QuestionsContainer:
    """Questions controller, contains all question frames and can switch between them"""

    def __init__(self, controller, student_name, questions):
        self.controller = controller
        self.student_name = student_name
        self.questions = []
        self.current_question = -1

        for i, question in enumerate(questions):
            answers = [
                {"answer": question["wrong_answer_1"], "correct": False},
                {"answer": question["wrong_answer_2"], "correct": False},
                {"answer": question["wrong_answer_3"], "correct": False},
                {"answer": question["right_answer"], "correct": True},
            ]
            # show answers in random order
            random.shuffle(answers)
            # create frame for ech question
            self.questions.append(
                QuestionPage(
                    parent=controller,
                    questions_container=self,
                    question_text=question["question"],
                    subject_name=question["subject_name"],
                    picture_id=question["picture_id"],
                    answers=answers,
                    is_last_question=True if i == len(questions) - 1 else False,
                )
            )

    def show_next_question(self):
        """Switch to another question frame"""
        self.current_question += 1
        if self.questions[self.current_question].picture_id:
            # wait for picture question download if picture is presented
            self.questions[self.current_question].picture_thread.join()
        self.controller.switch_frame_by_instance(self.questions[self.current_question])
        return

Methods

def show_next_question(self)

Switch to another question frame

Expand source code
def show_next_question(self):
    """Switch to another question frame"""
    self.current_question += 1
    if self.questions[self.current_question].picture_id:
        # wait for picture question download if picture is presented
        self.questions[self.current_question].picture_thread.join()
    self.controller.switch_frame_by_instance(self.questions[self.current_question])
    return
class ReadCardPage (parent, *args, **kwargs)

Read card page

Construct a frame widget with the parent MASTER.

Valid resource names: background, bd, bg, borderwidth, class, colormap, container, cursor, height, highlightbackground, highlightcolor, highlightthickness, relief, takefocus, visual, width.

Expand source code
class ReadCardPage(tk.Frame, ActionInterface):
    """Read card page"""

    def __init__(self, parent, *args, **kwargs):
        super(ReadCardPage, self).__init__(parent)
        self.parent = parent
        self.read_card_label = tk.Label(  # Read label
            self,
            text="Přiložte vstupní kartu",
            fg="black",
            font=tkFont.Font(family="Helvetica", size=45, weight="bold"),
        )
        # Center label position and grid settings
        self.read_card_label.grid(column=1, row=1, sticky="nsew")
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(1, weight=1)

    def post_init_actions(self):
        """Plan periodic card read"""
        read_card(self.read_card_label, self.parent)

Ancestors

  • tkinter.Frame
  • tkinter.Widget
  • tkinter.BaseWidget
  • tkinter.Misc
  • tkinter.Pack
  • tkinter.Place
  • tkinter.Grid
  • ActionInterface
  • abc.ABC

Methods

def post_init_actions(self)

Plan periodic card read

Expand source code
def post_init_actions(self):
    """Plan periodic card read"""
    read_card(self.read_card_label, self.parent)
class VerifyCardPage (parent, *args, **kwargs)

Verify card number page

Construct a frame widget with the parent MASTER.

Valid resource names: background, bd, bg, borderwidth, class, colormap, container, cursor, height, highlightbackground, highlightcolor, highlightthickness, relief, takefocus, visual, width.

Expand source code
class VerifyCardPage(tk.Frame, ActionInterface):
    """Verify card number page"""

    def __init__(self, parent, *args, **kwargs):
        super(VerifyCardPage, self).__init__(parent)
        self.parent = parent
        self.card_number = kwargs.get("card_number")
        # Info label
        self.verify_card_label = tk.Label(
            self,
            text="Karta načtena. Probíhá ověřování její platnosti.",
            fg="black",
            font=tkFont.Font(family="Helvetica", size=45, weight="bold"),
            wraplength=1000,
            justify=tk.CENTER,
        )
        self.verify_card_label.grid(column=1, row=1)
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(1, weight=1)

    def post_init_actions(self):
        """Start async backend call and start monitoring if this operation finish"""
        backend_thread = AsyncBackendCommunicator(
            url=QUESTIONS_URL,
            daemon=True,
            params=dict(
                card_number=self.card_number,
                question_count=NUMBER_OF_QUESTIONS,
                device_id=DEVICE_ID,
            ),
        )
        backend_thread.start()
        self.monitor_backend_thread(backend_thread)

    def monitor_backend_thread(self, thread):
        """Check if async thread is finished. If it is then handle result and switch
        frame if it is not finished then plan periodic result check.

        Args:
            thread (AsyncBackendCommunicator): Started backend call thread

        """
        if thread.is_alive():
            # thread still working, plan periodic check after 100ms
            self.after(100, lambda: self.monitor_backend_thread(thread))
        else:
            if thread.is_result_ok:
                # thread finished and questions were downloaded
                if not thread.result.get("questions"):
                    self.parent.switch_frame_by_class(
                        ErrorPage,
                        error_type=Errors.NO_QUESTIONS_ERROR,
                    )
                    return
                # init questions container and show first question
                questions_container = QuestionsContainer(
                    controller=self.parent,
                    student_name=thread.result["student_name"],
                    questions=thread.result["questions"],
                )
                questions_container.show_next_question()
                return
            else:
                # thread finished with error
                if thread.error_code == 404:
                    error_type = Errors.NO_ACTIVE_STUDY_ERROR
                elif thread.error_code == 403:
                    error_type = Errors.NO_ACCESS_ERROR
                else:
                    error_type = thread.error_type
                # show error page
                self.parent.switch_frame_by_class(
                    ErrorPage,
                    error_type=error_type,
                    error_code=thread.error_code,
                )
                return

Ancestors

  • tkinter.Frame
  • tkinter.Widget
  • tkinter.BaseWidget
  • tkinter.Misc
  • tkinter.Pack
  • tkinter.Place
  • tkinter.Grid
  • ActionInterface
  • abc.ABC

Methods

def monitor_backend_thread(self, thread)

Check if async thread is finished. If it is then handle result and switch frame if it is not finished then plan periodic result check.

Args

thread : AsyncBackendCommunicator
Started backend call thread
Expand source code
def monitor_backend_thread(self, thread):
    """Check if async thread is finished. If it is then handle result and switch
    frame if it is not finished then plan periodic result check.

    Args:
        thread (AsyncBackendCommunicator): Started backend call thread

    """
    if thread.is_alive():
        # thread still working, plan periodic check after 100ms
        self.after(100, lambda: self.monitor_backend_thread(thread))
    else:
        if thread.is_result_ok:
            # thread finished and questions were downloaded
            if not thread.result.get("questions"):
                self.parent.switch_frame_by_class(
                    ErrorPage,
                    error_type=Errors.NO_QUESTIONS_ERROR,
                )
                return
            # init questions container and show first question
            questions_container = QuestionsContainer(
                controller=self.parent,
                student_name=thread.result["student_name"],
                questions=thread.result["questions"],
            )
            questions_container.show_next_question()
            return
        else:
            # thread finished with error
            if thread.error_code == 404:
                error_type = Errors.NO_ACTIVE_STUDY_ERROR
            elif thread.error_code == 403:
                error_type = Errors.NO_ACCESS_ERROR
            else:
                error_type = thread.error_type
            # show error page
            self.parent.switch_frame_by_class(
                ErrorPage,
                error_type=error_type,
                error_code=thread.error_code,
            )
            return
def post_init_actions(self)

Start async backend call and start monitoring if this operation finish

Expand source code
def post_init_actions(self):
    """Start async backend call and start monitoring if this operation finish"""
    backend_thread = AsyncBackendCommunicator(
        url=QUESTIONS_URL,
        daemon=True,
        params=dict(
            card_number=self.card_number,
            question_count=NUMBER_OF_QUESTIONS,
            device_id=DEVICE_ID,
        ),
    )
    backend_thread.start()
    self.monitor_backend_thread(backend_thread)